package hudson.plugins.ec2.win.winrm;
import hudson.plugins.ec2.win.winrm.request.RequestFactory;
import hudson.plugins.ec2.win.winrm.soap.Namespaces;
import java.io.IOException;
import java.io.PipedOutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.ParseException;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.params.AuthPolicy;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.XPath;
import org.jaxen.SimpleNamespaceContext;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
public class WinRMClient {
private static final Logger log = Logger.getLogger(WinRMClient.class.getName());
private static final String APPLICATION_SOAP_XML = "application/soap+xml";
private final URL url;
private final String username;
private final String password;
private String shellId;
private String commandId;
private int exitCode;
private SimpleNamespaceContext namespaceContext;
private final RequestFactory factory;
private final ThreadLocal<BasicAuthCache> authCache = new ThreadLocal<BasicAuthCache>();
private boolean useHTTPS;
private Scheme httpsScheme;
private BasicCredentialsProvider credsProvider;
public WinRMClient(URL url, String username, String password) {
this.url = url;
this.username = username;
this.password = password;
this.factory = new RequestFactory(url);
setupHTTPClient();
}
public void openShell() {
log.log(Level.FINE, "opening winrm shell to: " + url);
Document request = factory.newOpenShellRequest().build();
shellId = first(sendRequest(request), "//*[@Name='ShellId']");
log.log(Level.FINER, "shellid: " + shellId);
}
public void executeCommand(String command) {
log.log(Level.FINE, "winrm execute on " + shellId + " command: " + command);
Document request = factory.newExecuteCommandRequest(shellId, command).build();
commandId = first(sendRequest(request), "//" + Namespaces.NS_WIN_SHELL.getPrefix() + ":CommandId");
log.log(Level.FINER, "winrm started execution on " + shellId + " commandId: " + commandId);
}
public void deleteShell() {
if (shellId == null) {
throw new IllegalStateException("no shell has been created");
}
log.log(Level.FINE, "closing winrm shell " + shellId);
Document request = factory.newDeleteShellRequest(shellId).build();
sendRequest(request);
}
public void signal() {
if (commandId == null) {
throw new IllegalStateException("no command is running");
}
log.log(Level.FINE, "signalling winrm shell " + shellId + " command: " + commandId);
Document request = factory.newSignalRequest(shellId, commandId).build();
sendRequest(request);
}
public void sendInput(byte[] input) {
log.log(Level.FINE, "--> sending " + input.length);
Document request = factory.newSendInputRequest(input, shellId, commandId).build();
sendRequest(request);
}
public boolean slurpOutput(PipedOutputStream stdout, PipedOutputStream stderr) throws IOException {
log.log(Level.FINE, "--> SlurpOutput");
ImmutableMap<String, PipedOutputStream> streams = ImmutableMap.of("stdout", stdout, "stderr", stderr);
Document request = factory.newGetOutputRequest(shellId, commandId).build();
Document response = sendRequest(request);
XPath xpath = DocumentHelper.createXPath("//" + Namespaces.NS_WIN_SHELL.getPrefix() + ":Stream");
namespaceContext = new SimpleNamespaceContext();
namespaceContext.addNamespace(Namespaces.NS_WIN_SHELL.getPrefix(), Namespaces.NS_WIN_SHELL.getURI());
xpath.setNamespaceContext(namespaceContext);
Base64 base64 = new Base64();
for (Element e : (List<Element>) xpath.selectNodes(response)) {
PipedOutputStream stream = streams.get(e.attribute("Name").getText().toLowerCase());
final byte[] decode = base64.decode(e.getText());
log.log(Level.FINE, "piping " + decode.length + " bytes from "
+ e.attribute("Name").getText().toLowerCase());
stream.write(decode);
}
XPath done = DocumentHelper.createXPath("//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']");
done.setNamespaceContext(namespaceContext);
if (Iterables.isEmpty(done.selectNodes(response))) {
log.log(Level.FINE, "keep going baby!");
return true;
} else {
exitCode = Integer.parseInt(first(response, "//" + Namespaces.NS_WIN_SHELL.getPrefix() + ":ExitCode"));
log.log(Level.FINE, "no more output - command is now done - exit code: " + exitCode);
}
return false;
}
public int exitCode() {
return exitCode;
}
private static String first(Document doc, String selector) {
XPath xpath = DocumentHelper.createXPath(selector);
try {
return Iterables.get((List<Element>) xpath.selectNodes(doc), 0).getText();
} catch (IndexOutOfBoundsException e) {
throw new RuntimeException("Malformed response for " + selector + " in " + doc.asXML());
}
}
private void setupHTTPClient() {
credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT), new UsernamePasswordCredentials(username, password));
}
private DefaultHttpClient buildHTTPClient() {
DefaultHttpClient httpclient = new DefaultHttpClient();
if(! (username.contains("\\")|| username.contains("/"))){
//user is not a domain user
httpclient.getAuthSchemes().register(AuthPolicy.SPNEGO,new NegotiateNTLMSchemaFactory());
}
httpclient.setCredentialsProvider(credsProvider);
httpclient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 5000);
// httpclient.setHttpRequestRetryHandler(new WinRMRetryHandler());
return httpclient;
}
private Document sendRequest(Document request) {
return sendRequest(request, 0);
}
private Document sendRequest(Document request, int retry) {
if (retry > 3) {
throw new RuntimeException("Too many retry for request");
}
DefaultHttpClient httpclient = buildHTTPClient();
HttpContext context = new BasicHttpContext();
if (authCache.get() == null) {
authCache.set(new BasicAuthCache());
}
context.setAttribute(ClientContext.AUTH_CACHE, authCache.get());
if (useHTTPS) {
httpclient.getConnectionManager().getSchemeRegistry().register(httpsScheme);
}
try {
HttpPost post = new HttpPost(url.toURI());
HttpEntity entity = new StringEntity(request.asXML(), APPLICATION_SOAP_XML, "UTF-8");
post.setEntity(entity);
log.log(Level.FINEST, "Request:\nPOST " + url + "\n" + request.asXML());
HttpResponse response = httpclient.execute(post, context);
HttpEntity responseEntity = response.getEntity();
if (response.getStatusLine().getStatusCode() != 200) {
// check for possible timeout
if (response.getStatusLine().getStatusCode() == 500
&& (responseEntity.getContentType() != null && entity.getContentType().getValue().startsWith(APPLICATION_SOAP_XML))) {
String respStr = EntityUtils.toString(responseEntity);
if (respStr.contains("TimedOut")) {
return DocumentHelper.parseText(respStr);
}
} else {
// this shouldn't happen, as httpclient knows how to auth
// the request
// but I've seen it. I blame keep-alive, so we're just going
// to scrap the connections, and try again
if (response.getStatusLine().getStatusCode() == 401) {
// we need to force using new connections here
// throw away our auth cache
log.log(Level.WARNING, "winrm returned 401 - shouldn't happen though - retrying in 2 minutes");
try {
Thread.sleep(TimeUnit.MINUTES.toMillis(3));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
authCache.set(new BasicAuthCache());
log.log(Level.WARNING, "winrm returned 401 - retrying now");
return sendRequest(request, ++retry);
}
log.log(Level.WARNING, "winrm service " + shellId + " unexpected HTTP Response ("
+ response.getStatusLine().getReasonPhrase() + "): "
+ EntityUtils.toString(response.getEntity()));
throw new RuntimeException("Unexpected HTTP response " + response.getStatusLine().getStatusCode()
+ " on " + url + ": " + response.getStatusLine().getReasonPhrase());
}
}
if (responseEntity.getContentType() == null
|| !entity.getContentType().getValue().startsWith(APPLICATION_SOAP_XML)) {
throw new RuntimeException("Unexepected WinRM content type: " + entity.getContentType());
}
Document responseDocument = DocumentHelper.parseText(EntityUtils.toString(responseEntity));
log.log(Level.FINEST, "Response:\n" + responseDocument.asXML());
return responseDocument;
} catch (URISyntaxException e) {
throw new RuntimeException("Invalid WinRM URI " + url);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Invalid WinRM body " + request.asXML());
} catch (ClientProtocolException e) {
throw new RuntimeException("HTTP Error " + e.getMessage(), e);
} catch (HttpHostConnectException e) {
log.log(Level.SEVERE, "Can't connect to host", e);
throw new WinRMConnectException("Can't connect to host: " + e.getMessage(), e);
} catch (IOException e) {
log.log(Level.SEVERE, "I/O Exception in HTTP POST", e);
throw new RuntimeIOException("I/O Exception " + e.getMessage(), e);
} catch (ParseException e) {
log.log(Level.SEVERE, "XML Parse exception in HTTP POST", e);
throw new RuntimeException("Unparseable XML in winRM response " + e.getMessage(), e);
} catch (DocumentException e) {
log.log(Level.SEVERE, "XML Document exception in HTTP POST", e);
throw new RuntimeException("Invalid XML document in winRM response " + e.getMessage(), e);
}
}
public String getTimeout() {
return factory.getTimeout();
}
public void setTimeout(String timeout) {
factory.setTimeout(timeout);
}
public void setUseHTTPS(boolean useHTTPS) {
this.useHTTPS = useHTTPS;
if (useHTTPS) {
SSLSocketFactory socketFactory;
try {
socketFactory = new SSLSocketFactory(new TrustSelfSignedStrategy(), new AllowAllHostnameVerifier());
httpsScheme = new Scheme("https", 443, socketFactory);
} catch (KeyManagementException e) {
} catch (UnrecoverableKeyException e) {
} catch (NoSuchAlgorithmException e) {
} catch (KeyStoreException e) {
}
}else{
httpsScheme=null;
}
}
}